import { GlSkeletonLoader, GlTable, GlTruncate, GlFormCheckbox } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { Portal } from 'portal-vue';
import { nextTick } from 'vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/shared/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/shared/empty_states/filters_produced_no_results.vue';
import IssuesBadge from 'ee/security_dashboard/components/shared/issues_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/shared/vulnerability_report/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/shared/vulnerability_comment_icon.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
  FIELD_PRESETS,
  FIELDS,
} from 'ee/security_dashboard/components/shared/vulnerability_report/constants';
import { stubComponent } from 'helpers/stub_component';
import {
  clusterImageScanningVulnerability,
  generateVulnerabilities,
  vulnerabilities,
} from '../../mock_data';

const portalName = 'portal-name';

describe('Vulnerability list component', () => {
  let wrapper;

  const createWrapper = ({ props = {}, listeners, provide = {}, stubs } = {}) => {
    wrapper = mountExtended(VulnerabilityList, {
      propsData: {
        vulnerabilities: [],
        fields: FIELD_PRESETS.DEVELOPMENT,
        portalName,
        pageSize: 20,
        ...props,
      },
      stubs: {
        GlPopover: true,
        SelectionSummary: true,
        Portal: {
          template: '<div><slot></slot></div>',
        },
        ...stubs,
      },
      listeners,
      provide: () => ({
        dashboardType: DASHBOARD_TYPES.PROJECT,
        noVulnerabilitiesSvgPath: '#',
        emptyStateSvgPath: '#',
        hasVulnerabilities: true,
        hasJiraVulnerabilitiesIntegrationEnabled: false,
        canAdminVulnerability: true,
        ...provide,
      }),
    });
  };

  const locationText = ({ file, startLine }) => `${file}:${startLine}`;
  const findTable = () => wrapper.findComponent(GlTable);
  const findCell = (label) => wrapper.find(`.js-${label}`);
  const findRows = () => wrapper.findAll('tbody tr');
  const findRow = (index = 0) => findRows().at(index);
  const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]');
  const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index);
  const findRemediatedBadge = () => wrapper.findComponent(RemediatedBadge);
  const findSelectionSummary = () => wrapper.findComponent(SelectionSummary);
  const findRowVulnerabilityCommentIcon = (row) =>
    findRow(row).findComponent(VulnerabilityCommentIcon);
  const findDataCell = (label) => wrapper.findByTestId(label);
  const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
  const findClusterCell = (id) => wrapper.findByTestId(`cluster-${id}`);
  const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`);
  const findTitleCell = (id) => wrapper.findByTestId(`title-${id}`);
  const findLocationTextWrapper = (cell) => cell.findComponent(GlTruncate);
  const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
  const findDashboardHasNoVulnerabilities = () =>
    wrapper.findComponent(DashboardHasNoVulnerabilities);
  const findVendorNames = () => wrapper.findByTestId('vulnerability-vendor');
  const findCheckAllCheckbox = () => wrapper.findByTestId('vulnerability-checkbox-all');
  const findAllRowCheckboxes = () => wrapper.findAllByTestId('vulnerability-checkbox');
  const findSkeletonLoading = () => wrapper.findAllComponents(GlSkeletonLoader);

  describe('with vulnerabilities', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render a list of vulnerabilities', () => {
      expect(wrapper.findAll('.js-status')).toHaveLength(newVulnerabilities.length);
    });

    it('should correctly render the status', () => {
      const cell = findCell('status');

      expect(cell.text()).toBe(capitalize(newVulnerabilities[0].state));
    });

    it('should correctly render the severity', () => {
      const cell = findCell('severity');
      expect(cell.text().toLowerCase()).toBe(newVulnerabilities[0].severity);
    });

    it('should correctly render the description', () => {
      const cell = findCell('description');
      expect(cell.text()).toBe(newVulnerabilities[0].title);
    });

    it('should display the remediated badge', () => {
      expect(findRemediatedBadge().exists()).toBe(true);
    });

    it('should display autoFixIcon for first Item', () => {
      expect(findAutoFixBulbInRow(findRow(0)).exists()).toBe(true);
    });

    it('should not display autoFixIcon for second Item', () => {
      expect(findAutoFixBulbInRow(findRow(1)).exists()).toBe(false);
    });

    it('should correctly render the identifier cell', () => {
      const identifiers = findDataCells('vulnerability-identifier');
      const extraIdentifierCounts = findDataCells('vulnerability-more-identifiers');

      const firstIdentifiers = newVulnerabilities[0].identifiers;
      expect(identifiers.at(0).text()).toBe(firstIdentifiers[0].name);
      expect(trimText(extraIdentifierCounts.at(0).text())).toContain(
        `${firstIdentifiers.length - 1} more`,
      );

      expect(identifiers.at(1).text()).toBe(newVulnerabilities[1].identifiers[0].name);
      expect(extraIdentifierCounts).toHaveLength(1);
    });

    it('should correctly render the report type cell', () => {
      const cells = findDataCells('vulnerability-report-type');
      expect(cells.at(0).text()).toBe('SAST');
      expect(cells.at(1).text()).toBe('Dependency Scanning');
      expect(cells.at(2).text()).toBe('Custom scanner without translation');
      expect(cells.at(3).text()).toBe('');
    });

    it('should correctly render the vulnerability vendor if the vulnerability vendor does exist', () => {
      const cells = findDataCells('vulnerability-vendor');
      expect(cells.at(0).text()).toBe('GitLab');
    });

    it('should correctly render an empty string if the vulnerability vendor does not exist', () => {
      const cells = findDataCells('vulnerability-vendor');
      expect(cells.at(3).text()).toBe('');
    });

    it('should portal the selection summary to the expected portal', () => {
      expect(wrapper.findComponent(Portal).attributes('to')).toBe(portalName);
    });

    it('should not show the selection summary if no vulnerabilities are selected', () => {
      expect(findSelectionSummary().props('visible')).toBe(false);
    });

    it('should show the selection summary when a checkbox is selected', async () => {
      findDataCell('vulnerability-checkbox').setChecked(true);
      await nextTick();

      expect(findSelectionSummary().props('visible')).toBe(true);
    });

    it('passes the selected vulnerabilities to the selection summary', async () => {
      await findRow(0).trigger('click');
      await findRow(1).trigger('click');
      await findRow(2).trigger('click');

      expect(findSelectionSummary().props('selectedVulnerabilities')).toEqual([
        newVulnerabilities[0],
        newVulnerabilities[1],
        newVulnerabilities[2],
      ]);
    });

    it('should sync selected vulnerabilities when the vulnerability list is updated', async () => {
      findDataCell('vulnerability-checkbox').setChecked(true);

      await nextTick();

      expect(findSelectionSummary().props('selectedVulnerabilities')).toHaveLength(1);

      wrapper.setProps({ vulnerabilities: [] });

      await nextTick();

      expect(findSelectionSummary().props('visible')).toBe(false);
    });

    it('should uncheck a selected vulnerability after the vulnerability is updated', async () => {
      const checkbox = () => findDataCell('vulnerability-checkbox');
      checkbox().setChecked(true);
      expect(checkbox().element.checked).toBe(true);

      await nextTick();
      findSelectionSummary().vm.$emit('vulnerability-updated', newVulnerabilities[0].id);
      await nextTick();

      expect(checkbox().element.checked).toBe(false);
    });

    describe.each([true, false])(
      'issues badge when "hasJiraVulnerabilitiesIntegrationEnabled" is set to "%s"',
      (hasJiraVulnerabilitiesIntegrationEnabled) => {
        beforeEach(() => {
          createWrapper({
            props: { vulnerabilities: generateVulnerabilities() },
            provide: { hasJiraVulnerabilitiesIntegrationEnabled },
          });
        });

        it('should display the issues badge for the first item', () => {
          expect(findIssuesBadge(0).exists()).toBe(true);
        });

        it('should not display the issues badge for the second item', () => {
          expect(() => findIssuesBadge(1)).toThrow();
        });

        it('should render the badge as Jira issues', () => {
          expect(findIssuesBadge(0).props('isJira')).toBe(hasJiraVulnerabilitiesIntegrationEnabled);
        });
      },
    );
  });

  describe('when user has no permission to admin vulnerabilities', () => {
    beforeEach(() => {
      createWrapper({
        props: { vulnerabilities },
        provide: { canAdminVulnerability: false },
      });
    });

    it('should not show the checkboxes', () => {
      expect(findDataCell('vulnerability-checkbox-all').exists()).toBe(false);
      expect(findDataCell('vulnerability-checkbox').exists()).toBe(false);
    });

    it('should not select a clicked vulnerability', async () => {
      findRow(1).trigger('click');
      await nextTick();

      expect(findSelectionSummary().props()).toMatchObject({
        visible: false,
        selectedVulnerabilities: [],
      });
    });
  });

  describe('when displayed on instance or group level dashboard', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      createWrapper({
        props: { vulnerabilities: newVulnerabilities, shouldShowProjectNamespace: true },
      });
    });

    it('should display the vulnerability locations for images', () => {
      const { id, project, location } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: location.image,
          position: 'middle',
        }),
      );
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: locationText(location),
          position: 'middle',
        }),
      );
    });

    it('should display the vulnerability locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: location.file,
          position: 'middle',
        }),
      );
    });

    it('should not display the vulnerability locations for vulnerabilities without a location', () => {
      const { id, project } = newVulnerabilities[4];
      const cellText = findLocationCell(id).text();
      expect(cellText).toEqual(project.nameWithNamespace);
      expect(cellText).not.toContain(':');
    });

    it('should display the vulnerability locations for path', () => {
      const { id, project, location } = newVulnerabilities[5];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: location.path,
          position: 'middle',
        }),
      );
    });
  });

  describe('when displayed on a project level dashboard', () => {
    let newVulnerabilities;
    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowIdentifier: true,
          shouldShowReportType: true,
        },
      });
    });

    it('should not display the vulnerability group/project locations for images', () => {
      const { id, project, location } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: location.image,
          position: 'middle',
        }),
      );
    });

    it('should display the detected time', () => {
      const { id } = newVulnerabilities[1];
      const cell = findDataCell(`detected-${id}`);
      expect(cell.text()).toEqual(`2020-07-22`);
      expect(cell.attributes('title')).toEqual('Jul 22, 2020 7:31pm UTC');
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: locationText(location),
          position: 'middle',
        }),
      );
    });

    it('should make the file path linkable', () => {
      const { id, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.find('a').attributes('href')).toBe(`${location.blobPath}#L${location.startLine}`);
    });

    it('should not make the file path linkable if blobPath is missing', () => {
      const { id } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.find('a').exists()).toBe(false);
    });

    it('should not display the vulnerability group/project locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual(
        expect.objectContaining({
          text: location.file,
          position: 'middle',
        }),
      );
    });
  });

  describe('when has an issue associated', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].issueLinks = {
        nodes: [
          {
            issue: {
              title: 'my-title',
              iid: 114,
              state: 'opened',
              webUrl: 'http://localhost/issues/~/114',
            },
          },
        ],
      };
      createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should emit "vulnerability-clicked" with the vulnerability as a payload when a vulnerability-link is clicked', async () => {
      const clickedEventName = 'vulnerability-clicked';
      const vulnerability = newVulnerabilities[1];
      const link = findTitleCell(vulnerability.id).find('a');

      expect(wrapper.emitted(clickedEventName)).toBe(undefined);

      await link.trigger('click');
      const emittedEvents = wrapper.emitted(clickedEventName);

      expect(emittedEvents).toHaveLength(1);
      expect(emittedEvents[0][0]).toBe(vulnerability);
    });
  });

  describe('when has comments', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].userNotesCount = 1;
      createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render the comments badge on the first vulnerability', () => {
      expect(findRowVulnerabilityCommentIcon(0).exists()).toBe(true);
    });

    it('should not render the comments badge on the second vulnerability', () => {
      expect(findRowVulnerabilityCommentIcon(1).exists()).toBe(false);
    });
  });

  describe('when GitLab is the only scanner in the reports', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities = newVulnerabilities.map((v) => ({
        ...v,
        scanner: { vendor: 'GitLab' },
      }));
      createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(false);
    });
  });

  describe('when vendor name is not provided in the reports', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities = newVulnerabilities.map((v) => ({ ...v, scanner: { vendor: '' } }));
      createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(false);
    });
  });

  describe('when there are other scanners in the report', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].scanner = { vendor: 'GitLab' };
      newVulnerabilities[1].scanner = { vendor: 'Third Party Scanner' };
      createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(true);
    });
  });

  describe('when a vulnerability has a false positive', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].falsePositive = true;
      createWrapper({
        props: { vulnerabilities: newVulnerabilities },
        provide: {
          canViewFalsePositive: true,
        },
      });
    });

    it('should render the false positive info badge on the first vulnerability', () => {
      const row = findRow(0);
      const badge = row.findComponent(FalsePositiveBadge);

      expect(badge.exists()).toEqual(true);
    });

    it('should not render the false positive info badge on the second vulnerability', () => {
      const row = findRow(1);
      const badge = row.findComponent(FalsePositiveBadge);

      expect(badge.exists()).toEqual(false);
    });
  });

  describe('when a vulnerability is resolved on the default branch', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].resolvedOnDefaultBranch = true;
      createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render the remediated info badge on the first vulnerability', () => {
      const row = findRow(0);
      const badge = row.findComponent(RemediatedBadge);

      expect(badge.exists()).toEqual(true);
    });

    it('should not render the remediated info badge on the second vulnerability', () => {
      const row = findRow(1);
      const badge = row.findComponent(RemediatedBadge);

      expect(badge.exists()).toEqual(false);
    });
  });

  describe('loading prop', () => {
    it.each`
      phrase        | isLoading
      ${'show'}     | ${true}
      ${'not show'} | ${false}
    `('should $phrase the loading state', ({ isLoading }) => {
      createWrapper({ props: { isLoading, vulnerabilities } });

      expect(findCell('status').exists()).toEqual(!isLoading);
      expect(findSkeletonLoading().exists()).toBe(isLoading);
    });
  });

  describe('with no vulnerabilities', () => {
    beforeEach(() => {
      createWrapper();
    });

    it('should show the empty state', () => {
      expect(findCell('status').exists()).toEqual(false);
      expect(findFiltersProducedNoResults().exists()).toEqual(true);
      expect(findDashboardHasNoVulnerabilities().exists()).toEqual(false);
    });
  });

  describe('sorting', () => {
    it('passes the sort prop to the table', () => {
      const sort = { sortBy: 'a', sortDesc: true };
      createWrapper({ stubs: { GlTable: true }, props: { sort } });

      expect(findTable().attributes()).toMatchObject({
        'sort-by': sort.sortBy,
        'sort-desc': sort.sortDesc.toString(),
      });
    });

    it('emits sort data in expected format', () => {
      createWrapper();

      const sort = { sortBy: 'state', sortDesc: true };
      findTable().vm.$emit('sort-changed', sort);

      expect(wrapper.emitted('update:sort')[0][0]).toEqual(sort);
    });
  });

  describe('row click', () => {
    const findRowCheckbox = (index) =>
      findRow(index).find('[data-testid="vulnerability-checkbox"]');

    beforeEach(() => {
      createWrapper({ props: { vulnerabilities } });
    });

    it('will select and deselect vulnerabilities', async () => {
      const rowCount = vulnerabilities.length;
      const rowsToClick = [0, 1, 2];
      const clickRows = () => rowsToClick.forEach((row) => findRow(row).trigger('click'));
      const expectRowCheckboxesToBe = (condition) => {
        for (let i = 0; i < rowCount; i += 1)
          expect(findRowCheckbox(i).element.checked).toBe(condition(i));
      };

      clickRows();
      await nextTick();
      expectRowCheckboxesToBe((i) => rowsToClick.includes(i));

      clickRows();
      await nextTick();
      expectRowCheckboxesToBe(() => false);
    });
  });

  describe('select all checkbox', () => {
    it('will toggle between selecting all and deselecting all vulnerabilities', async () => {
      const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true);

      createWrapper({ props: { vulnerabilities } });
      // Sanity check to ensure that everything starts off unchecked.
      expect(getChecked()).toHaveLength(0);

      await findCheckAllCheckbox().setChecked(true);
      // First click should select all rows.
      expect(getChecked()).toHaveLength(vulnerabilities.length);

      await findCheckAllCheckbox().setChecked(false);
      // Second click should un-select all rows.
      expect(getChecked()).toHaveLength(0);
    });

    it('will toggle the indeterminate state when some but not all vulnerabilities are selected', async () => {
      const expectIndeterminateState = (state) =>
        expect(findCheckAllCheckbox().props('indeterminate')).toBe(state);

      createWrapper({
        props: { vulnerabilities },
        stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) },
      });

      // We start off with no items selected, so no indeterminate state.
      expectIndeterminateState(false);

      await findRow(1).trigger('click');
      // When we go from 0 to 1 item selected, indeterminate state should be true.
      expectIndeterminateState(true);

      await findRow(1).trigger('click');
      // When we go from 1 to 0 items selected, indeterminate state should be false.
      expectIndeterminateState(false);

      // Check all items.
      findCheckAllCheckbox().trigger('click');
      // When all the items are selected, indeterminate state should be false.
      expectIndeterminateState(false);

      await findRow(1).trigger('click');
      // When we uncheck an item when all items are selected, indeterminate state should be true.
      expectIndeterminateState(true);
    });
  });

  describe('fields prop', () => {
    it('shows the expected columns in the table', () => {
      const { STATUS, SEVERITY } = FIELDS;
      const fields = [STATUS, SEVERITY];
      createWrapper({
        props: { fields, vulnerabilities },
        provide: { canAdminVulnerability: false },
      });

      // Check that there are only 2 columns.
      expect(findRow().element.cells).toHaveLength(2);
      expect(findCell(STATUS.class).exists()).toBe(true);
      expect(findCell(SEVERITY.class).exists()).toBe(true);
    });
  });

  describe('pageSize prop', () => {
    it('shows the same number of skeleton loaders as the pageSize prop', () => {
      const pageSize = 17;
      createWrapper({ props: { pageSize, isLoading: true } });

      expect(findSkeletonLoading()).toHaveLength(pageSize);
    });
  });

  describe('operational vulnerabilities', () => {
    beforeEach(() => {
      createWrapper({
        props: {
          fields: FIELD_PRESETS.OPERATIONAL,
          vulnerabilities: [clusterImageScanningVulnerability],
        },
      });
    });

    it('shows the cluster column', () => {
      expect(findClusterCell(clusterImageScanningVulnerability.id).exists()).toBe(true);
    });
  });
});
